/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.remote;

import java.awt.*;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.renderable.RenderableImage;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
import org.eclipse.imagen.*;
import org.eclipse.imagen.util.CaselessStringKey;
import org.eclipse.imagen.util.ImagingException;
import org.eclipse.imagen.util.ImagingListener;

/**
 * A convenience class for instantiating operations on remote machines.
 *
 * <p>This class also provides information related to the server and allows for setting of parameters for the remote
 * communication with the server.
 *
 * <p>Conceptually this class is very similar to the <code>JAI</code> class, except that the <code>RemoteJAI</code>
 * class deals with remote operations. This class allows programmers to use the syntax:
 *
 * <pre>
 * import org.eclipse.imagen.remote.RemoteJAI;
 * RemoteJAI rc = new RemoteJAI(protocolName, serverName);
 * RemoteRenderedOp im = rc.create("convolve", paramBlock, renderHints);
 * </pre>
 *
 * to create new images by applying operators that are executed remotely on the specified server. The <code>create()
 * </code> method returns a <code>RemoteRenderedOp</code> encapsulating the protocol name, server name, operation name,
 * parameter block, and rendering hints. Additionally, it performs validity checking on the operation parameters. The
 * operation parameters are determined from the <code>OperationDescriptor</code> retrieved using the <code>
 * getServerSupportedOperationList()</code> method. Programmers may also refer to RemoteJAI.createRenderable("opname",
 * paramBlock, renderHints);
 *
 * <p>If the <code>OperationDescriptor</code> associated with the named operation returns <code>true</code> from its
 * <code>isImmediate()</code> method, the <code>create()</code> method will ask the <code>RemoteRenderedOp</code> it
 * constructs to render itself immediately. If this rendering is <code>null</code>, <code>create()</code> will itself
 * return <code>null</code> rather than returning an instance of <code>RemoteRenderedOp</code> as it normally does.
 *
 * <p>The registry being used by this class may be inspected or set using the <code>getOperationRegistry()</code> and
 * <code>setOperationRegistry()</code> methods. Only experienced users should attempt to set the registry. This registry
 * is used to map protocol names into either a <code>RemoteRIF</code> or a <code>RemoteCRIF</code>.
 *
 * <p>The <code>TileCache</code> associated with an instance may be similarly accessed.
 *
 * <p>Each instance of <code>RemoteJAI</code> contains a set of default rendering hints which will be used for all image
 * creations. These hints are merged with any hints supplied to the <code>create</code> method; directly supplied hints
 * take precedence over the common hints. When a new <code>RemoteJAI</code> instance is constructed, its hints are
 * initialized to a copy of the default hints. Thus when an instance of <code>RemoteJAI</code> is constructed, hints for
 * the default registry, tile cache, number of retries, and the retry interval are added to the set of common rendering
 * hints. Similarly, invoking <code>setOperationRegistry()</code>, <code>setTileCache()</code>, <code>setNumRetries()
 * </code> or <code>setRetryInterval()</code> on a <code>RemoteJAI</code> instance will cause the respective entity to
 * be added to the common rendering hints. The hints associated with any instance may be manipulated using the <code>
 * getRenderingHints()</code>, <code>setRenderingHints()</code>, <and <code>clearRenderingHints()</code> methods.
 *
 * <p>The <code>TileCache</code> to be used by a particular operation may be set during construction, or by calling the
 * <code>setTileCache()</code> method. This will result in the provided tile cache being added to the set of common
 * rendering hints.
 *
 * <p>Network errors are dealt with through the use of retry intervals and retries. Retries refers to the maximum number
 * of times a remote operation will be retried. The retry interval refers to the amount of time (in milliseconds)
 * between two consecutive retries. If errors are encountered at each retry and the number of specified retries has been
 * exhausted, a <code>RemoteImagingException</code> will be thrown. By default, the number of retries is set to five,
 * and the retry interval is set to a thousand milliseconds. These values can be changed by using the <code>
 * setNumRetries()</code> and the <code>setRetryInterval</code> methods and can also be specified via the <code>
 * RenderingHints</code> object passed as an argument to <code>RemoteJAI.create()</code>. Time outs (When the amount of
 * time taken to get a response or the result of an operation from the remote machine exceeds a limit) are not dealt
 * with, and must be taken care of by the network imaging protocol implementation itself. The implementation must be
 * responsible for monitoring time outs, but on encountering one can deal with it by throwing a <code>
 * RemoteImagingException</code>, which will then be dealt with using retries and retry intervals.
 *
 * <p>This class provides the capability of negotiating capabilities between the client and the server. The <code>
 * negotiate</code> method uses the preferences specified via the <code>setNegotiationPreferences</code> method
 * alongwith the server and client capabilities retrieved via the <code>getServerCapabilities</code> and <code>
 * getClientCapabilities</code> respectively to negotiate on each of the preferences. This negotiation treats the client
 * and server capabilities as being non-preferences, and the user set <code>NegotiableCapabilitySet</code> as being a
 * preference. The negotiation is performed according to the rules described in the class documentation for <code>
 * NegotiableCapability</code>.
 *
 * <p>Note that negotiation preferences can be set either prior to specifying a particular rendered or renderable
 * operation (by using <code>RemoteJAI.create()</code> or <code>RemoteJAI.createRenderable()</code>) or afterwards. The
 * currently set negotiation preferences are passed to the <code>RemoteRenderedOp</code> on its construction through the
 * <code>RenderingHints</code> using the <code>KEY_NEGOTIATION_PREFERENCES</code> key. Since <code>RemoteRenderableOp
 * </code> does not accept a <code>RenderingHints</code> object as a construction argument, the newly created <code>
 * RemoteRenderableOp</code> is informed of these preferences using it's <code>setRenderingHints()</code> method. These
 * preferences can be changed after the construction using the <code>setNegotiationPreferences()</code> method on both
 * <code>RemoteRenderedOp</code> and <code>RemoteRenderableOp</code>.
 *
 * <p>The same behavior applies to the number of retries and the retry interval, whether they be the default values
 * contained in the default <code>RenderingHints</code> or whether they are set using the <code>setNumRetries</code> or
 * <code>setRetryInterval</code> methods, the existing values are passed to <code>RemoteRenderedOp</code>'s when they
 * are created through the <code>RenderingHints</code> argument, and are set on the newly created <code>
 * RemoteRenderableOp</code> using the <code>setNumRetries</code> or <code>setRetryInterval</code> methods on <code>
 * RemoteRenderableOp</code>.
 *
 * @see JAI
 * @see JAIRMIDescriptor
 * @see RemoteImagingException
 * @since JAI 1.1
 */
public class RemoteJAI {

    /** An inner class defining rendering hint keys. */
    static class RenderingKey extends RenderingHints.Key {
        // cache the class of JAI to keep RemoteJAI.class in memory unless
        // the class RenderingKey is GC'ed.  In this case, the
        // WeakReferences in the map of RenderingHints.Key will release
        // the instances of RenderingKey.  So when JAI is loaded next
        // time, the keys can be recreated without any exception.
        // Fix bug: 4754807
        private static Class JAIclass = RemoteJAI.class;

        private Class objectClass;

        RenderingKey(int privateKey, Class objectClass) {
            super(privateKey);
            this.objectClass = objectClass;
        }

        public boolean isCompatibleValue(Object val) {
            return objectClass.isInstance(val);
        }
    }

    private static final int HINT_RETRY_INTERVAL = 115;
    private static final int HINT_NUM_RETRIES = 116;
    private static final int HINT_NEGOTIATION_PREFERENCES = 117;

    /**
     * Key for the retry interval value to be used for dealing with network errors during remote imaging. The
     * corresponding object must be an <code>Integer</code>. The common <code>RenderingHints</code> do not contain a
     * default hint corresponding to this key.
     *
     * @see RemoteJAI
     * @since JAI 1.1
     */
    public static RenderingHints.Key KEY_RETRY_INTERVAL =
            new RemoteJAI.RenderingKey(HINT_RETRY_INTERVAL, Integer.class);
    /**
     * Key for the number of retries to be used for dealing with network errors during remote imaging. The corresponding
     * object must be an <code>Integer</code>. The common <code>RenderingHints</code> do not contain a default hint
     * corresponding to this key.
     *
     * @see RemoteJAI
     * @since JAI 1.1
     */
    public static RenderingHints.Key KEY_NUM_RETRIES = new RemoteJAI.RenderingKey(HINT_NUM_RETRIES, Integer.class);
    /**
     * Key for the negotiation preferences to be used to negotiate capabilities to be used in the remote communication.
     * The corresponding object must be a <code>NegotiableCapabilitySet</code>. The common <code>RenderingHints</code>
     * do not contain a default hint corresponding to this key.
     *
     * @see RemoteJAI
     * @since JAI 1.1
     */
    public static RenderingHints.Key KEY_NEGOTIATION_PREFERENCES =
            new RemoteJAI.RenderingKey(HINT_NEGOTIATION_PREFERENCES, NegotiableCapabilitySet.class);
    /** The String representing the remote server machine. */
    protected String serverName;

    /** The name of the protocol used for client-server communication. */
    protected String protocolName;

    /** The OperationRegistry instance used for instantiating operations. */
    private OperationRegistry operationRegistry = JAI.getDefaultInstance().getOperationRegistry();

    /** The amount of time to wait between retries (in Millseconds). */
    public static final int DEFAULT_RETRY_INTERVAL = 1000;

    /** The default number of retries. */
    public static final int DEFAULT_NUM_RETRIES = 5;

    /** Time in milliseconds between retries, initialized to default value. */
    private int retryInterval = DEFAULT_RETRY_INTERVAL; // Milliseconds

    /** The number of retries, initialized to default value. */
    private int numRetries = DEFAULT_NUM_RETRIES;

    /** A reference to a centralized TileCache object. */
    private transient TileCache cache = JAI.getDefaultInstance().getTileCache();

    /** The RenderingHints object used to retrieve the TileCache, OperationRegistry hints. */
    private RenderingHints renderingHints;

    /** The set of preferences to be used for the communication between the client and the server. */
    private NegotiableCapabilitySet preferences = null;

    /**
     * The set of properties agreed upon after the negotiation process between the client and the server has been
     * completed.
     */
    private static NegotiableCapabilitySet negotiated;

    /** The client and server capabilities. */
    private NegotiableCapabilitySet serverCapabilities = null;

    private NegotiableCapabilitySet clientCapabilities = null;

    /** A Hashtable containing OperationDescriptors hashed by their operation names. */
    private Hashtable odHash = null;

    /** The array of descriptors supported by the server. */
    private OperationDescriptor descriptors[] = null;

    /** Required to I18N compound messages. */
    private static MessageFormat formatter;

    /**
     * Constructs a <code>RemoteJAI</code> instance with the given protocol name and server name. The semantics of the
     * serverName are defined by the particular protocol used to create this class. Instructions on how to create a
     * serverName that is compatible with this protocol can be retrieved from the <code>getServerNameDocs()</code>
     * method on the <code>RemoteDescriptor</code> associated with the given protocolName. An <code>
     * IllegalArgumentException</code> may be thrown by the protocol specific classes at a later point, if null is
     * provided as the serverName argument and null is not considered a valid serverName by the specified protocol.
     *
     * @param protocolName The <code>String</code> that identifies the remote imaging protocol.
     * @param serverName The <code>String</code> that identifies the server.
     * @throws IllegalArgumentException if protocolName is null.
     */
    public RemoteJAI(String protocolName, String serverName) {
        this(protocolName, serverName, null, null);
    }

    /**
     * Constructs a <code>RemoteJAI</code> instance with the given protocol name, server name, <code>OperationRegistry
     * </code> and <code>TileCache</code>. If the specified <code>OperationRegistry</code> is null, the registry
     * associated with the default <code>JAI</code> instance will be used. If the specified <code>TileCache</code> is
     * null, the <code>TileCache</code> associated with the default <code>JAI</code> instance will be used.
     *
     * <p>An <code>IllegalArgumentException</code> may be thrown by the protocol specific classes at a later point, if
     * null is provided as the serverName argument and null is not considered a valid serverName by the specified
     * protocol.
     *
     * @param serverName The <code>String</code> that identifies the server.
     * @param protocolName The <code>String</code> that identifies the remote imaging protocol.
     * @param operationRegistry The <code>OperationRegistry</code> associated with this class, if null, default will be
     *     used.
     * @param tileCache The <code>TileCache</code> associated with this class, if null, default will be used.
     * @throws IllegalArgumentException if protocolName is null.
     */
    public RemoteJAI(String protocolName, String serverName, OperationRegistry registry, TileCache tileCache) {

        if (protocolName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        // For formatting error strings.
        formatter = new MessageFormat("");
        formatter.setLocale(Locale.getDefault());

        this.protocolName = protocolName;
        this.serverName = serverName;

        // operationRegistry and cache variables are already initialized
        // via static initializers, so change them only if the user has
        // provided a non-null value for them.
        if (registry != null) {
            this.operationRegistry = registry;
        }

        if (tileCache != null) {
            this.cache = tileCache;
        }

        this.renderingHints = new RenderingHints(null);
        this.renderingHints.put(JAI.KEY_OPERATION_REGISTRY, operationRegistry);
        this.renderingHints.put(JAI.KEY_TILE_CACHE, cache);
        this.renderingHints.put(KEY_RETRY_INTERVAL, new Integer(retryInterval));
        this.renderingHints.put(KEY_NUM_RETRIES, new Integer(numRetries));
    }

    /** Returns a <code>String</code> identifying the remote server machine. */
    public String getServerName() {
        return serverName;
    }

    /** Returns the protocol name. */
    public String getProtocolName() {
        return protocolName;
    }

    /**
     * Sets the amount of time between retries in milliseconds. The specified <code>retryInterval</code> parameter will
     * be added to the common <code>RenderingHints</code> of this <code>RemoteJAI</code> instance, under the <code>
     * JAI.KEY_RETRY_INTERVAL</code> key.
     *
     * @param retryInterval The time interval between retries (milliseconds).
     * @throws IllegalArgumentException if retryInterval is negative.
     */
    public void setRetryInterval(int retryInterval) {

        if (retryInterval < 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic3"));
        }

        this.retryInterval = retryInterval;
        renderingHints.put(KEY_RETRY_INTERVAL, new Integer(retryInterval));
    }

    /** Returns the amount of time between retries in milliseconds. */
    public int getRetryInterval() {
        return retryInterval;
    }

    /**
     * Sets the number of retries. The specified <code>numRetries</code> parameter will be added to the common <code>
     * RenderingHints</code> of this <code>RemoteJAI</code> instance, under the <code>JAI.KEY_NUM_RETRIES</code> key.
     *
     * @param numRetries The number of retries.
     * @throws IllegalArgumentException if numRetries is negative.
     */
    public void setNumRetries(int numRetries) {
        if (numRetries < 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic4"));
        }

        this.numRetries = numRetries;
        renderingHints.put(KEY_NUM_RETRIES, new Integer(numRetries));
    }

    /** Returns the number of retries. */
    public int getNumRetries() {
        return numRetries;
    }

    /** Returns the <code>OperationRegistry</code> being used by this <code>RemoteJAI</code> instance. */
    public OperationRegistry getOperationRegistry() {
        return operationRegistry;
    }

    /**
     * Sets the<code>OperationRegistry</code> to be used by this <code>RemoteJAI</code> instance. The <code>
     * operationRegistry</code> parameter will be added to the <code>RenderingHints</code> of this <code>RemoteJAI
     * </code> instance.
     *
     * @throws IllegalArgumentException if operationRegistry is null.
     */
    public void setOperationRegistry(OperationRegistry operationRegistry) {
        if (operationRegistry == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI4"));
        }
        this.operationRegistry = operationRegistry;
        this.renderingHints.put(JAI.KEY_OPERATION_REGISTRY, operationRegistry);
    }

    /**
     * Sets the <code>TileCache</code> to be used by this <code>RemoteJAI</code>. The <code>tileCache</code> parameter
     * will be added to the <code>RenderingHints</code> of this <code>RemoteJAI</code> instance.
     *
     * @throws IllegalArgumentException if tileCache is null.
     */
    public void setTileCache(TileCache tileCache) {
        if (tileCache == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI5"));
        }
        this.cache = tileCache;
        renderingHints.put(JAI.KEY_TILE_CACHE, cache);
    }

    /** Returns the <code>TileCache</code> being used by this <code>RemoteJAI</code> instance. */
    public TileCache getTileCache() {
        return cache;
    }

    /**
     * Returns the <code>RenderingHints</code> associated with this <code>RemoteJAI</code> instance. These rendering
     * hints will be merged with any hints supplied as an argument to the <code>create()</code> method.
     */
    public RenderingHints getRenderingHints() {
        return renderingHints;
    }

    /**
     * Sets the <code>RenderingHints</code> associated with this <code>RemoteJAI</code> instance. These rendering hints
     * will be merged with any hints supplied as an argument to the <code>create()</code> method.
     *
     * @throws IllegalArgumentException if hints is null.
     */
    public void setRenderingHints(RenderingHints hints) {
        if (hints == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI6"));
        }
        this.renderingHints = hints;
    }

    /** Clears the <code>RenderingHints</code> associated with this <code>RemoteJAI</code> instance. */
    public void clearRenderingHints() {
        this.renderingHints = new RenderingHints(null);
    }

    /**
     * Returns the hint value associated with a given key in this <code>RemoteJAI</code> instance, or <code>null</code>
     * if no value is associated with the given key.
     *
     * @throws IllegalArgumentException if key is null.
     */
    public Object getRenderingHint(RenderingHints.Key key) {
        if (key == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI7"));
        }
        return renderingHints.get(key);
    }

    /**
     * Sets the hint value associated with a given key in this <code>RemoteJAI</code> instance.
     *
     * @throws IllegalArgumentException if <code>key</code> is <code>null</code>.
     * @throws IllegalArgumentException if <code>value</code> is <code>null</code>.
     * @throws IllegalArgumentException if <code>value</code> is not of the correct type for the given hint.
     */
    public void setRenderingHint(RenderingHints.Key key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI7"));
        }
        if (value == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI8"));
        }

        try {
            renderingHints.put(key, value);
        } catch (Exception e) {
            throw new IllegalArgumentException(e.toString());
        }
    }

    /** Removes the hint value associated with a given key in this <code>RemoteJAI</code> instance. */
    public void removeRenderingHint(RenderingHints.Key key) {
        renderingHints.remove(key);
    }

    /**
     * Creates a <code>RemoteRenderedOp</code> which represents the named operation to be performed remotely, using the
     * source(s) and/or parameter(s) specified in the <code>ParameterBlock</code>, and applying the specified hints to
     * the destination. This method should only be used when the final result returned is a single <code>
     * RemoteRenderedImage</code>.
     *
     * <p>The supplied operation name is validated against the names of the <code>OperationDescriptor</code>s returned
     * from the <code>getServerSupportedOperationList()</code> method. The source(s) and/or parameter(s) in the <code>
     * ParameterBlock</code> are validated against the named operation's descriptor, both in their numbers and types.
     * Additional restrictions placed on the sources and parameters by an individual operation are also validated by
     * calling its <code>OperationDescriptor.validateArguments()</code> method.
     *
     * <p>Parameters are allowed to have a <code>null</code> input value, if that particular parameter has a default
     * value specified in its operation's descriptor. In this case, the default value will replace the <code>null</code>
     * input.
     *
     * <p>Unspecified tailing parameters are allowed, if these parameters have default values specified in the
     * operation's descriptor. However, if a parameter, which has a default value, is followed by one or more parameters
     * that have no default values, this parameter must be specified in the <code>ParameterBlock</code>, even if it only
     * has a value of code>null</code>.
     *
     * <p>The rendering hints associated with this instance of <code>RemoteJAI</code> are overlaid with the hints passed
     * to this method. That is, the set of keys will be the union of the keys from the instance's hints and the hints
     * parameter. If the same key exists in both places, the value from the hints parameter will be used.
     *
     * @param opName The name of the operation.
     * @param args The source(s) and/or parameter(s) for the operation.
     * @param hints The hints for the operation.
     * @throws IllegalArgumentException if <code>opName</code> is <code>null</code>.
     * @throws IllegalArgumentException if <code>args</code> is <code>null</code>.
     * @throws IllegalArgumentException if no <code>OperationDescriptor</code> is available from the server with the
     *     specified operation name.
     * @throws IllegalArgumentException if the <code>OperationDescriptor</code> for the specified operation name on the
     *     server does not support the "rendered" registry mode.
     * @throws IllegalArgumentException if the specified operation does not produce a <code>java.awt.image.RenderedImage
     *     </code>.
     * @throws IllegalArgumentException if the specified operation is unable to handle the sources and parameters
     *     specified in <code>args</code>.
     * @return A <code>RemoteRenderedOp</code> that represents the named operation to be performed remotely, or <code>
     *     null</code> if the specified operation is in the "immediate" mode and the rendering of the <code>PlanarImage
     *     </code> failed.
     */
    public RemoteRenderedOp create(String opName, ParameterBlock args, RenderingHints hints) {

        if (opName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI9"));
        }

        if (args == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI10"));
        }

        // Initialize the odHash hashtable
        getServerSupportedOperationList();

        // Get the OperationDescriptor associated with this name.
        OperationDescriptor odesc = (OperationDescriptor) odHash.get(new CaselessStringKey(opName));

        if (odesc == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI11"));
        }

        // Does this operation support rendered mode?
        if (!odesc.isModeSupported("rendered")) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI12"));
        }

        // Does the operation produce a RenderedImage?
        if (!RenderedImage.class.isAssignableFrom(odesc.getDestClass("rendered"))) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI13"));
        }

        // Validate input arguments. The ParameterBlock is cloned here
        // because OperationDescriptor.validateArguments() may change
        // its content.
        StringBuffer msg = new StringBuffer();
        args = (ParameterBlock) args.clone();
        if (!odesc.validateArguments("rendered", args, msg)) {
            throw new IllegalArgumentException(msg.toString());
        }

        // Merge rendering hints.  Hints passed in take precedence.
        RenderingHints mergedHints;
        if (hints == null) {
            mergedHints = renderingHints;
        } else if (renderingHints.isEmpty()) {
            mergedHints = hints;
        } else {
            mergedHints = new RenderingHints((Map) renderingHints);
            mergedHints.add(hints);
        }

        RemoteRenderedOp op =
                new RemoteRenderedOp(operationRegistry, protocolName, serverName, opName, args, mergedHints);

        // If the operation requests immediate rendering, do so.
        if (odesc.isImmediate()) {
            PlanarImage im = null;
            im = op.getRendering();

            if (im == null) {
                // Op could not be rendered, return null.
                return null;
            }
        }

        // Return the RemoteRenderedOp associated with this operation.
        return op;
    }

    /**
     * Creates a <code>RemoteRenderableOp</code> that represents the named operation to be performed remotely, using the
     * source(s) and/or parameter(s) specified in the <code>ParameterBlock</code>. This method should only be used when
     * the final result returned is a single <code>RenderableImage</code>.
     *
     * <p>The supplied operation name is validated against the names of the <code>OperationDescriptor</code>s returned
     * from the <code>getServerSupportedOperationList()</code> method. The source(s) and/or parameter(s) in the <code>
     * ParameterBlock</code> are validated against the named operation's descriptor, both in their numbers and types.
     * Additional restrictions placed on the sources and parameters by an individual operation are also validated by
     * calling its <code>OperationDescriptor.validateRenderableArguments()</code> method.
     *
     * <p>Parameters are allowed to have a <code>null</code> input value, if that particular parameter has a default
     * value specified in its operation's descriptor. In this case, the default value will replace the <code>null</code>
     * input.
     *
     * <p>Unspecified tailing parameters are allowed, if these parameters have default values specified in the
     * operation's descriptor. However, if a parameter, which has a default value, is followed by one or more parameters
     * that have no default values, this parameter must be specified in the <code>ParameterBlock</code>, even if it only
     * has a value of code>null</code>.
     *
     * @param opName The name of the operation.
     * @param args The source(s) and/or parameter(s) for the operation.
     * @throws IllegalArgumentException if <code>opName</code> is <code>null</code>.
     * @throws IllegalArgumentException if <code>args</code> is <code>null</code>.
     * @throws IllegalArgumentException if no <code>OperationDescriptor</code> is available from the server with the
     *     specified operation name.
     * @throws IllegalArgumentException if the <code>OperationDescriptor</code> for the specified operation name on the
     *     server does not support "renderable" registry mode.
     * @throws IllegalArgumentException if the specified operation does not produce a <code>
     *     java.awt.image.renderable.RenderableImage</code>.
     * @throws IllegalArgumentException if the specified operation is unable to handle the sources and parameters
     *     specified in <code>args</code>.
     * @return A <code>RemoteRenderableOp</code> that represents the named operation to be performed remotely.
     */
    public RemoteRenderableOp createRenderable(String opName, ParameterBlock args) {

        if (opName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI9"));
        }

        if (args == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI10"));
        }

        // Initialize the odHash hashtable
        getServerSupportedOperationList();

        // Get the OperationDescriptor associated with this name.
        OperationDescriptor odesc = (OperationDescriptor) odHash.get(new CaselessStringKey(opName));

        if (odesc == null) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI11"));
        }

        // Does this operation support rendered mode?
        if (!odesc.isModeSupported("renderable")) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI14"));
        }

        // Does the operation produce a RenderedImage?
        if (!RenderableImage.class.isAssignableFrom(odesc.getDestClass("renderable"))) {
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI15"));
        }

        // Validate input arguments. The ParameterBlock is cloned here
        // because OperationDescriptor.validateRenderableArguments()
        // may change its content.
        StringBuffer msg = new StringBuffer();
        args = (ParameterBlock) args.clone();
        if (!odesc.validateArguments("renderable", args, msg)) {
            throw new IllegalArgumentException(msg.toString());
        }

        RemoteRenderableOp op = new RemoteRenderableOp(operationRegistry, protocolName, serverName, opName, args);
        // Set the node-scope hints
        op.setRenderingHints(renderingHints);

        // Return the RemoteRenderableOp.
        return op;
    }

    //
    // NEGOTIATION RELATED METHODS
    //

    /**
     * Sets the preferences to be used in the client-server communication. These preferences are utilized in the
     * negotiation process. Note that preferences for more than one category can be specified using this method since
     * <code>NegotiableCapabilitySet</code> allows different <code>NegotiableCapability</code> objects to be bundled up
     * in one <code>NegotiableCapabilitySet</code> class. Even under the same category (as specified by the
     * getCategory() method on <code>NegotiableCapability</code>), multiple <code>NegotiableCapability</code> objects
     * can be added to the preferences. The preference added first for a particular category is given highest priority
     * in the negotiation process.
     *
     * <p>Since a new set of preferences is set everytime this method is called, this method allows for changing
     * negotiation preferences multiple times. However it should be noted that preferences set on this method are
     * relevant only prior to the creation of an operation (using the <code>RemoteJAI.create</code> method). To change
     * negotiation preferences on an operation after it has been created, the <code>setNegotiationPreferences()</code>
     * method on the created <code>RemoteRenderedOp</code> should be used. The <code>preferences</code> parameter will
     * be added to the <code>RenderingHints</code> of this <code>RemoteJAI</code> instance.
     */
    public void setNegotiationPreferences(NegotiableCapabilitySet preferences) {

        this.preferences = preferences;

        if (preferences == null) renderingHints.remove(KEY_NEGOTIATION_PREFERENCES);
        else renderingHints.put(KEY_NEGOTIATION_PREFERENCES, preferences);

        // Every time new preferences are set, invalidate old Negotiation
        // results and do the negotiation again.
        negotiated = null;
        getNegotiatedValues();
    }

    /**
     * Returns the results of the negotiation between the client and server capabilities according to the user
     * preferences specified at an earlier time. This will return null if the negotiation failed.
     *
     * <p>If a negotiation cycle has not been initiated prior to calling this method, or the negotiation preferences
     * have been changed, this method will initiate a new negotiation cycle, which will create and return a new set of
     * negotiated values.
     *
     * @returns A <code>NegotiableCapabilitySet</code> that is the result of the negotiation process, if negotiation is
     *     successful, otherwise returns null.
     */
    public NegotiableCapabilitySet getNegotiatedValues() throws RemoteImagingException {

        // If negotiation was not performed before, or if new preferences
        // have invalidated the old negotiated results.
        if (negotiated == null) {

            if (serverCapabilities == null) {
                serverCapabilities = getServerCapabilities();
            }

            if (clientCapabilities == null) {
                clientCapabilities = getClientCapabilities();
            }

            // Do the negotiation
            negotiated = negotiate(preferences, serverCapabilities, clientCapabilities);
        }

        return negotiated;
    }

    /**
     * Returns the results of the negotiation between the client and server capabilities according to the user
     * preferences specified at an earlier time for the given category. This method returns a <code>NegotiableCapability
     * </code> object, that represents the result of the negotiation for the given category. If the negotiation failed,
     * null will be returned.
     *
     * <p>If a negotiation cycle has not been initiated prior to calling this method, or the negotiation preferences
     * have been changed, this method will initiate a new negotiation cycle, which will create and return a new
     * negotiated value for the given category.
     *
     * @param category The category to negotiate on.
     * @throws IllegalArgumentException if category is null.
     * @returns A <code>NegotiableCapabilitySet</code> that is the result of the negotiation process, if negotiation is
     *     successful, otherwise returns null.
     */
    public NegotiableCapability getNegotiatedValues(String category) throws RemoteImagingException {

        // We do not need to check for category being null, since that
        // check will be made by the methods called from within this method.

        // If negotiation was not performed before, or if new preferences
        // have invalidated the old negotiated results.
        if (negotiated == null) {

            if (serverCapabilities == null) {
                serverCapabilities = getServerCapabilities();
            }

            if (clientCapabilities == null) {
                clientCapabilities = getClientCapabilities();
            }

            // Do the negotiation
            return negotiate(preferences, serverCapabilities, clientCapabilities, category);
        } else {
            // If negotiated is not null, then the negotiated results are
            // current and the result for the given category can just be
            // extracted from there and returned.
            return negotiated.getNegotiatedValue(category);
        }
    }

    /**
     * This method negotiates the capabilities to be used in the remote communication. Upon completion of the
     * negotiation process, this method returns a <code>NegotiableCapabilitySet</code> which contains an aggregation of
     * the <code>NegotiableCapability</code> objects that represent the results of negotiation. If the negotiation
     * fails, null will be returned.
     *
     * <p>The negotiation process treats the serverCapabilities and the clientCapabilities as non-preferences and will
     * throw an <code>IllegalArgumentException</code> if the <code>isPreference</code> method for either of these
     * returns true. The preferences <code>NegotiableCapabilitySet</code> should return true from its <code>isPreference
     * </code> method, otherwise an <code>IllegalArgumentException</code> will be thrown. The negotiation is done in
     * accordance with the rules described in the class comments for <code>NegotiableCapability</code>.
     *
     * <p>If either the serverCapabilities or the clientCapabilities is null, then the negotiation will fail, and null
     * will be returned. If preferences is null, the negotiation will become a two-way negotiation between the two
     * non-null <code>NegotiableCapabilitySet</code>s.
     *
     * @param preferences The user preferences for the negotiation.
     * @param serverCapabilities The capabilities of the server.
     * @param clientCapabilities The capabilities of the client.
     * @throws IllegalArgumentException if serverCapabilities is a preference, i.e., if it's <code>isPreference()</code>
     *     method returns true.
     * @throws IllegalArgumentException if clientCapabilities is a preference, i.e., if it's <code>isPreference()</code>
     *     method returns true.
     * @throws IllegalArgumentException if preferences is a non-preference, i.e., if it's <code>isPreference()</code>
     *     method returns false.
     */
    public static NegotiableCapabilitySet negotiate(
            NegotiableCapabilitySet preferences,
            NegotiableCapabilitySet serverCapabilities,
            NegotiableCapabilitySet clientCapabilities) {

        if (serverCapabilities == null || clientCapabilities == null) return null;

        if (serverCapabilities != null && serverCapabilities.isPreference() == true)
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI20"));

        if (clientCapabilities != null && clientCapabilities.isPreference() == true)
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI21"));

        if (preferences == null) {
            return serverCapabilities.negotiate(clientCapabilities);
        } else {
            if (preferences.isPreference() == false)
                throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI19"));

            NegotiableCapabilitySet clientServerCap = serverCapabilities.negotiate(clientCapabilities);
            if (clientServerCap == null) return null;
            return clientServerCap.negotiate(preferences);
        }
    }

    /**
     * This method negotiates the capabilities to be used in the remote communication for the given category. Upon
     * completion of the negotiation process, this method returns a <code>NegotiableCapability</code> object, that
     * represents the result of the negotiation for the given category. If the negotiation fails, null will be returned.
     *
     * <p>The negotiation process treats the serverCapabilities and the clientCapabilities as non-preferences and will
     * throw an <code>IllegalArgumentException</code> if the <code>isPreference</code> method for either of these
     * returns true. The preferences <code>NegotiableCapabilitySet</code> should return true from its <code>isPreference
     * </code> method or an <code>IllegalArgumentException</code> will be thrown. The negotiation is done in accordance
     * with the rules described in the class comments for <code>NegotiableCapability</code>.
     *
     * <p>If either the serverCapabilities or the clientCapabilities is null, then the negotiation will fail, and null
     * will be returned. If preferences is null, the negotiation will become a two-way negotiation between the two
     * non-null <code>NegotiableCapabilitySet</code>s.
     *
     * @param preferences The user preferences for the negotiation.
     * @param serverCapabilities The capabilities of the server.
     * @param clientCapabilities The capabilities of the client.
     * @param category The category to perform the negotiation on.
     * @throws IllegalArgumentException if preferences is a non-preference, i.e., if it's <code>isPreference()</code>
     *     method returns false.
     * @throws IllegalArgumentException if serverCapabilities is a preference, i.e., if it's <code>isPreference()</code>
     *     method returns true.
     * @throws IllegalArgumentException if clientCapabilities is a preference, i.e., if it's <code>isPreference()</code>
     *     method returns true.
     * @throws IllegalArgumentException if category is null.
     */
    public static NegotiableCapability negotiate(
            NegotiableCapabilitySet preferences,
            NegotiableCapabilitySet serverCapabilities,
            NegotiableCapabilitySet clientCapabilities,
            String category) {

        if (serverCapabilities == null || clientCapabilities == null) return null;

        if (serverCapabilities != null && serverCapabilities.isPreference() == true)
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI20"));

        if (clientCapabilities != null && clientCapabilities.isPreference() == true)
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI21"));

        if (preferences != null && preferences.isPreference() == false)
            throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI19"));

        if (category == null) throw new IllegalArgumentException(JaiI18N.getString("RemoteJAI26"));

        if (preferences == null || preferences.isEmpty()) {
            return serverCapabilities.getNegotiatedValue(clientCapabilities, category);
        } else {

            List prefList = preferences.get(category);
            List serverList = serverCapabilities.get(category);
            List clientList = clientCapabilities.get(category);
            Iterator p = prefList.iterator();

            NegotiableCapability server, client, result;

            NegotiableCapability pref = null;
            // If there are no preferences for the current category
            if (p.hasNext() == false) pref = null;
            else pref = (NegotiableCapability) p.next();

            Vector results = new Vector();

            // Negotiate every server NC with every client NC
            for (Iterator s = serverList.iterator(); s.hasNext(); ) {
                server = (NegotiableCapability) s.next();
                for (Iterator c = clientList.iterator(); c.hasNext(); ) {
                    client = (NegotiableCapability) c.next();

                    result = server.negotiate(client);
                    if (result == null) {
                        // This negotiation failed, continue to the next one
                        continue;
                    } else {
                        // Negotiation between client and server succeeded,
                        // add to results array
                        results.add(result);

                        if (pref != null) {
                            // Negotiate with the pref, if negotiation is
                            // successful, return the result from this method.
                            result = result.negotiate(pref);
                        }

                        if (result != null) {
                            return result;
                        } // else move onto next negotiation
                    }
                }
            }

            for (; p.hasNext(); ) {
                pref = (NegotiableCapability) p.next();
                for (int r = 0; r < results.size(); r++) {
                    if ((result = pref.negotiate((NegotiableCapability) results.elementAt(r))) != null) {
                        return result;
                    }
                }
            }

            // If all negotiations failed, return null.
            return null;
        }
    }

    /**
     * Returns the set of capabilites supported by the server. If any network related errors are encountered by this
     * method (identified as such by receiving a <code>RemoteImagingException</code>), they will be dealt with by the
     * use of retries and retry intervals.
     */
    public NegotiableCapabilitySet getServerCapabilities() throws RemoteImagingException {

        if (serverCapabilities == null) {

            // Get the RemoteDescriptor for protocolName
            RemoteDescriptor descriptor =
                    (RemoteDescriptor) operationRegistry.getDescriptor(RemoteDescriptor.class, protocolName);

            if (descriptor == null) {
                Object[] msgArg0 = {new String(protocolName)};
                formatter.applyPattern(JaiI18N.getString("RemoteJAI16"));
                throw new RuntimeException(formatter.format(msgArg0));
            }
            Exception rieSave = null;
            int count = 0;
            while (count++ < numRetries) {
                try {
                    serverCapabilities = descriptor.getServerCapabilities(serverName);
                    break;
                } catch (RemoteImagingException rie) {
                    // Print that an Exception occured
                    System.err.println(JaiI18N.getString("RemoteJAI24"));
                    rieSave = rie;
                    // Sleep for retryInterval milliseconds
                    try {
                        Thread.sleep(retryInterval);
                    } catch (InterruptedException ie) {
                        sendExceptionToListener(
                                JaiI18N.getString("Generic5"), new ImagingException(JaiI18N.getString("Generic5"), ie));
                        //			throw new RuntimeException(ie.toString());
                    }
                }
            }

            if (serverCapabilities == null && count > numRetries) {
                sendExceptionToListener(JaiI18N.getString("RemoteJAI18"), rieSave);
                //		throw new RemoteImagingException(
                //					   JaiI18N.getString("RemoteJAI18")+"\n"+rieSave.getMessage());
            }
        }

        return serverCapabilities;
    }

    /** Returns the set of capabilities supported by the client. */
    public NegotiableCapabilitySet getClientCapabilities() {

        if (clientCapabilities == null) {

            RemoteRIF rrif = (RemoteRIF) operationRegistry.getFactory("remoteRendered", protocolName);
            if (rrif == null) {
                rrif = (RemoteRIF) operationRegistry.getFactory("remoteRenderable", protocolName);
            }

            if (rrif == null) {
                Object[] msgArg0 = {new String(protocolName)};
                formatter.applyPattern(JaiI18N.getString("RemoteJAI17"));
                throw new RuntimeException(formatter.format(msgArg0));
            }

            clientCapabilities = rrif.getClientCapabilities();
        }

        return clientCapabilities;
    }

    /**
     * Returns the list of <code>OperationDescriptor</code>s that describe the operations supported by the server. If
     * any network related errors are encountered by this method (identified as such by receiving a <code>
     * RemoteImagingException</code>), they will be dealt with by the use of retries and retry intervals.
     */
    public OperationDescriptor[] getServerSupportedOperationList() throws RemoteImagingException {

        if (descriptors == null) {

            // Get the RemoteDescriptor for protocolName
            RemoteDescriptor descriptor =
                    (RemoteDescriptor) operationRegistry.getDescriptor(RemoteDescriptor.class, protocolName);

            if (descriptor == null) {
                Object[] msgArg0 = {new String(protocolName)};
                formatter.applyPattern(JaiI18N.getString("RemoteJAI16"));
                throw new RuntimeException(formatter.format(msgArg0));
            }
            Exception rieSave = null;
            int count = 0;
            while (count++ < numRetries) {
                try {
                    descriptors = descriptor.getServerSupportedOperationList(serverName);
                    break;
                } catch (RemoteImagingException rie) {
                    // Print that an Exception occured
                    System.err.println(JaiI18N.getString("RemoteJAI25"));
                    rieSave = rie;
                    // Sleep for retryInterval milliseconds
                    try {
                        Thread.sleep(retryInterval);
                    } catch (InterruptedException ie) {
                        //			throw new ImagingException(ie);
                        sendExceptionToListener(
                                JaiI18N.getString("Generic5"), new ImagingException(JaiI18N.getString("Generic5"), ie));
                    }
                }
            }

            if (descriptors == null && count > numRetries) {
                sendExceptionToListener(JaiI18N.getString("RemoteJAI23"), rieSave);
                //		throw new RemoteImagingException(
                //					   JaiI18N.getString("RemoteJAI23")+"\n"+rieSave.getMessage());
            }

            // Store the descriptors into a Hashtable hashed by
            // their operation name.
            odHash = new Hashtable();
            for (int i = 0; i < descriptors.length; i++) {
                odHash.put(new CaselessStringKey(descriptors[i].getName()), descriptors[i]);
            }
        }

        return descriptors;
    }

    void sendExceptionToListener(String message, Exception e) {
        ImagingListener listener = JAI.getDefaultInstance().getImagingListener();
        listener.errorOccurred(message, e, this, false);
    }
}
